// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
//
//   Keepass2Android is free software: you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   Keepass2Android is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with Keepass2Android.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Text;
using Android.Util;
using Android.Views;
using Android.Widget;
using Java.IO;
using Java.Net;
using keepass2android.Io;
using keepass2android.Utils;
using keepass2android;
using KeePassLib;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using Console = System.Console;
using Object = Java.Lang.Object;
using AndroidX.Core.Content;
using Uri = Android.Net.Uri;

namespace keepass2android
{
  [Activity(Label = AppNames.AppName,
      MainLauncher = false,
      Theme = "@style/Kp2aTheme_BlueNoActionBar",
      Exported = true,
      LaunchMode = LaunchMode.SingleInstance)] //caution, see manifest file
  public class SelectCurrentDbActivity : LifecycleAwareActivity
  {
    private int ReqCodeOpenNewDb = 1;

    private static bool IsValidIoc(AutoExecItem item)
    {
      IOConnectionInfo itemIoc;
      if (!KeeAutoExecExt.TryGetDatabaseIoc(item, out itemIoc))
        return false;
      try
      {
        //see if we have a file storage for the given protocol
        App.Kp2a.GetFileStorage(itemIoc);
      }
      catch (Exception e)
      {
        return false;
      }
      return true;
    }

    public class OpenDatabaseAdapter : BaseAdapter
    {

      private readonly SelectCurrentDbActivity _context;
      internal List<Database> _displayedDatabases;
      internal List<AutoExecItem> _autoExecItems;

      public OpenDatabaseAdapter(SelectCurrentDbActivity context)
      {
        _context = context;
        Update();

      }

      public override Object GetItem(int position)
      {
        return position;
      }

      public override long GetItemId(int position)
      {
        return position;
      }



      public static float convertDpToPixel(float dp, Context context)
      {
        return Util.convertDpToPixel(dp, context);
      }


      public override View GetView(int position, View convertView, ViewGroup parent)
      {

        Button btn;

        if (convertView == null)
        {
          // if it's not recycled, initialize some attributes

          btn = new Button(_context);
          btn.LayoutParameters = new GridView.LayoutParams((int)convertDpToPixel(140, _context),
              (int)convertDpToPixel(150, _context));

          btn.SetPadding((int)convertDpToPixel(4, _context),
              (int)convertDpToPixel(20, _context),
              (int)convertDpToPixel(4, _context),
              (int)convertDpToPixel(4, _context));
          btn.SetTextSize(ComplexUnitType.Sp, 11);
          btn.SetTextColor(new Color(115, 115, 115));
          btn.SetSingleLine(false);
          btn.Gravity = GravityFlags.Center;
          btn.Click += (sender, args) =>
          {
            int pos;
            int.TryParse(((Button)sender).Tag.ToString(), out pos);
            if (pos < _displayedDatabases.Count)
              _context.OnDatabaseSelected(_displayedDatabases[pos]);
            else if (pos < _displayedDatabases.Count + _autoExecItems.Count)
              _context.OnAutoExecItemSelected(_autoExecItems[pos - _displayedDatabases.Count]);
            else
              _context.OnOpenOther();

          };
        }
        else
        {
          btn = (Button)convertView;
        }

        btn.Tag = position.ToString();

        string displayName;
        Drawable drawable;
        if (position < _displayedDatabases.Count)
        {
          var db = _displayedDatabases[position];
          drawable = App.Kp2a.GetStorageIcon(Util.GetProtocolId(db.Ioc));
          displayName = db.KpDatabase.Name;
          displayName += "\n" + App.Kp2a.GetFileStorage(db.Ioc).GetDisplayName(db.Ioc);
          btn.SetBackgroundResource(Resource.Drawable.storagetype_button_bg);
        }
        else if (position < _displayedDatabases.Count + _autoExecItems.Count)
        {
          var item = _autoExecItems[position - _displayedDatabases.Count];
          drawable = App.Kp2a.GetResourceDrawable("baseline_file_open_24");
          displayName = item.Entry.Strings.ReadSafe(PwDefs.TitleField);
          btn.SetBackgroundResource(Resource.Drawable.storagetype_button_bg_dark);
        }
        else
        {
          btn.SetBackgroundResource(Resource.Drawable.storagetype_button_bg);
          displayName = _context.GetString(Resource.String.start_open_file);
          drawable = App.Kp2a.GetResourceDrawable("baseline_file_open_24");
        }

        var str = new SpannableString(displayName);

        btn.TextFormatted = str;
        //var drawable = ContextCompat.GetDrawable(context, Resource.Drawable.Icon);
        btn.SetCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null);

        return btn;
      }

      public override int Count
      {
        get { return _displayedDatabases.Count + _autoExecItems.Count + 1; }
      }

      public void Update()
      {
        string thisDevice = KeeAutoExecExt.ThisDeviceId;
        _displayedDatabases = App.Kp2a.OpenDatabases.ToList();
        _autoExecItems = App.Kp2a.OpenDatabases
            .SelectMany(db => KeeAutoExecExt.GetAutoExecItems(db.KpDatabase))
            .Where(item =>
                item.Visible
                &&
                KeeAutoExecExt.IsDeviceEnabled(item, thisDevice, out _)
                &&
                !_displayedDatabases.Any(displayedDb =>
                {
                  IOConnectionInfo itemIoc;
                  return KeeAutoExecExt.TryGetDatabaseIoc(item, out itemIoc) &&
                                 displayedDb.Ioc.IsSameFileAs(itemIoc);
                })
                && IsValidIoc(item)

                )

            .ToList();
      }


    }

    private void OnAutoExecItemSelected(AutoExecItem autoExecItem)
    {
      KeeAutoExecExt.AutoOpenEntry(this, autoExecItem, true, new ActivityLaunchModeSimple());
    }

    private void OnOpenOther()
    {
      StartFileSelect(true, true);
    }

    private void OnDatabaseSelected(Database selectedDatabase)
    {
      App.Kp2a.CurrentDb = selectedDatabase;
      LaunchingOther = true;
      AppTask.LaunchFirstGroupActivity(this);
    }

    public override bool OnCreateOptionsMenu(IMenu menu)
    {
      MenuInflater inflater = MenuInflater;
      inflater.Inflate(Resource.Menu.menu_selectdb, menu);
      return base.OnCreateOptionsMenu(menu);
    }

    public override bool OnOptionsItemSelected(IMenuItem item)
    {
      switch (item.ItemId)
      {
        case Resource.Id.menu_search_advanced:
          if (App.Kp2a.CurrentDb == null)
            App.Kp2a.CurrentDb = App.Kp2a.OpenDatabases.First();
          Intent i = new Intent(this, typeof(SearchActivity));
          AppTask.ToIntent(i);
          StartActivityForResult(i, 0);
          return true;
        case Resource.Id.menu_lock:
          App.Kp2a.Lock();
          return true;
        case Resource.Id.menu_donate:
          return Util.GotoDonateUrl(this);
        case Resource.Id.menu_app_settings:
          DatabaseSettingsActivity.Launch(this);
          return true;
        default:
          break;
      }
      return base.OnOptionsItemSelected(item);
    }

    protected override void OnCreate(Bundle savedInstanceState)
    {
      base.OnCreate(savedInstanceState);
      SetContentView(Resource.Layout.open_db_selection);

      var toolbar = FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.mytoolbar);

      SetSupportActionBar(toolbar);

      SupportActionBar.Title = GetString(Resource.String.select_database);


      //only load the AppTask if this is the "first" OnCreate (not because of kill/resume, i.e. savedInstanceState==null)
      // and if the activity is not launched from history (i.e. recent tasks) because this would mean that
      // the Activity was closed already (user cancelling the task or task complete) but is restarted due recent tasks.
      // Don't re-start the task (especially bad if tak was complete already)
      if (Intent.Flags.HasFlag(ActivityFlags.LaunchedFromHistory))
      {
        AppTask = new NullTask();
      }
      else
      {
        AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
      }

      _adapter = new OpenDatabaseAdapter(this);
      var gridView = FindViewById<GridView>(Resource.Id.gridview);

      gridView.Adapter = _adapter;

      if (!string.IsNullOrEmpty(Intent.GetStringExtra(Util.KeyFilename)))
      {
        IOConnectionInfo ioc = new IOConnectionInfo();
        Util.SetIoConnectionFromIntent(ioc, Intent);

        if (App.Kp2a.TrySelectCurrentDb(ioc))
        {
          if (!OpenAutoExecEntries(App.Kp2a.CurrentDb))
          {
            LaunchingOther = true;
            AppTask.CanActivateSearchViewOnStart = true;
            AppTask.LaunchFirstGroupActivity(this);
          }
        }
        else
        {
          //forward to password activity
          Intent i = new Intent(this, typeof(PasswordActivity));
          Util.PutIoConnectionToIntent(ioc, i);
          i.PutExtra(PasswordActivity.KeyKeyfile, Intent.GetStringExtra(PasswordActivity.KeyKeyfile));
          i.PutExtra(PasswordActivity.KeyPassword, Intent.GetStringExtra(PasswordActivity.KeyPassword));
          i.PutExtra(PasswordActivity.LaunchImmediately, Intent.GetBooleanExtra(PasswordActivity.LaunchImmediately, false));
          LaunchingOther = true;
          StartActivityForResult(i, ReqCodeOpenNewDb);
        }

      }
      else
      {

        if (Intent.Action == Intent.ActionView)
        {
          if (IsOtpUri(Intent.Data))
          {
            AppTask = new CreateEntryThenCloseTask()
            {
              AllFields = Newtonsoft.Json.JsonConvert.SerializeObject(new Dictionary<string, string>()
                            {
                                { "otp", Intent.DataString }
                            })
            };
          }
          else
          {
            GetIocFromViewIntent(Intent);
          }
        }
        else if (Intent.Action == Intent.ActionSend)
        {
          ActivationCondition activationCondition = ActivationCondition.Never;
          ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
          if (prefs.GetBoolean("kp2a_switch_rooted", false))
          {
            activationCondition = ActivationCondition.Always;
          }
          else
          {
            //if the app is about to be closed again (e.g. after searching for a URL and returning to the browser:
            // automatically bring up the Keyboard selection dialog
            if (prefs.GetBoolean(this.GetString(Resource.String.OpenKp2aKeyboardAutomatically_key), this.Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default)))
            {
              activationCondition = ActivationCondition.Always;
            }
          }

          AppTask = new SearchUrlTask()
          {
            UrlToSearchFor = Intent.GetStringExtra(Intent.ExtraText),
            ActivateKeyboard = activationCondition
          };
        }
      }

    }

    private bool IsOtpUri(Uri? uri)
    {
      return uri?.Scheme == "otpauth";
    }

    protected override void OnStart()
    {
      base.OnStart();

      if (_intentReceiver == null)
      {
        _intentReceiver = new MyBroadcastReceiver(this);
        IntentFilter filter = new IntentFilter();
        filter.AddAction(Intents.DatabaseLocked);
        filter.AddAction(Intent.ActionScreenOff);
        ContextCompat.RegisterReceiver(this, _intentReceiver, filter, (int)ReceiverFlags.Exported);
      }
    }

    protected override void OnStop()
    {
      if (_intentReceiver != null)
      {
        UnregisterReceiver(_intentReceiver);
        _intentReceiver = null;
      }
      base.OnStop();
    }

    private bool GetIocFromViewIntent(Intent intent)
    {
      IOConnectionInfo ioc = new IOConnectionInfo();

      //started from "view" intent (e.g. from file browser)
      ioc.Path = intent.DataString;

      if (ioc.Path.StartsWith("file://"))
      {
        ioc.Path = URLDecoder.Decode(ioc.Path.Substring(7));

        if (ioc.Path.Length == 0)
        {
          // No file name
          App.Kp2a.ShowMessage(this, Resource.String.FileNotFound, MessageSeverity.Error);
          return false;
        }

        Java.IO.File dbFile = new Java.IO.File(ioc.Path);
        if (!dbFile.Exists())
        {
          // File does not exist
          App.Kp2a.ShowMessage(this, Resource.String.FileNotFound, MessageSeverity.Error);
          return false;
        }
      }
      else
      {
        if (!ioc.Path.StartsWith("content://"))
        {
          App.Kp2a.ShowMessage(this, Resource.String.error_can_not_handle_uri, MessageSeverity.Error);
          return false;
        }
        IoUtil.TryTakePersistablePermissions(this.ContentResolver, intent.Data);


      }

      if (App.Kp2a.TrySelectCurrentDb(ioc))
      {
        if (OpenAutoExecEntries(App.Kp2a.CurrentDb)) return false;
        LaunchingOther = true;
        AppTask.CanActivateSearchViewOnStart = true;
        AppTask.LaunchFirstGroupActivity(this);
      }
      else
      {
        Intent launchIntent = new Intent(this, typeof(PasswordActivity));
        Util.PutIoConnectionToIntent(ioc, launchIntent);
        LaunchingOther = true;
        StartActivityForResult(launchIntent, ReqCodeOpenNewDb);
      }


      return true;
    }

    protected override void OnResume()
    {
      _isForeground = true;
      base.OnResume();
      if (!IsFinishing && !LaunchingOther)
      {
        if (App.Kp2a.OpenDatabases.Any() == false)
        {
          StartFileSelect(true);
          return;
        }

        //database loaded
        if (App.Kp2a.QuickLocked)
        {
          AppTask.CanActivateSearchViewOnStart = true;
          var i = new Intent(this, typeof(QuickUnlock));
          Util.PutIoConnectionToIntent(App.Kp2a.GetDbForQuickUnlock().Ioc, i);
          Kp2aLog.Log("Starting QuickUnlock");
          StartActivityForResult(i, 0);
          return;
        }

        //see if there are any AutoOpen items to open

        foreach (var db in App.Kp2a.OpenDatabases)
        {
          try
          {
            if (OpenAutoExecEntries(db)) return;
          }
          catch (Exception e)
          {
            App.Kp2a.ShowMessage(this, "Failed to open child databases", MessageSeverity.Error);
            Kp2aLog.LogUnexpectedError(e);
          }

        }

        //database(s) unlocked
        if ((App.Kp2a.OpenDatabases.Count() == 1) || (AppTask is SearchUrlTask))
        {
          LaunchingOther = true;
          AppTask.LaunchFirstGroupActivity(this);
          return;
        }


      }

      //more than one database open or user requested to load another db. Don't launch another activity.
      _adapter.Update();
      _adapter.NotifyDataSetChanged();

      base.OnResume();
    }

    private bool OpenAutoExecEntries(Database db)
    {
      try
      {
        string thisDevice = KeeAutoExecExt.ThisDeviceId;
        foreach (var autoOpenItem in KeeAutoExecExt.GetAutoExecItems(db.KpDatabase))
        {
          if (!autoOpenItem.Enabled)
            continue;
          if (!KeeAutoExecExt.IsDeviceEnabled(autoOpenItem, thisDevice, out _))
            continue;
          if (!IsValidIoc(autoOpenItem))
            continue;

          IOConnectionInfo dbIoc;
          if (KeeAutoExecExt.TryGetDatabaseIoc(autoOpenItem, out dbIoc) &&
              App.Kp2a.TryGetDatabase(dbIoc) == null &&
              App.Kp2a.AttemptedToOpenBefore(dbIoc) == false
          )
          {
            if (KeeAutoExecExt.AutoOpenEntry(this, autoOpenItem, false, new ActivityLaunchModeRequestCode(ReqCodeOpenNewDb)))
            {
              LaunchingOther = true;
              return true;
            }
          }
        }
      }
      catch (Exception e)
      {
        Kp2aLog.LogUnexpectedError(e);
      }

      return false;
    }

    protected override void OnPause()
    {
      LaunchingOther = false;
      _isForeground = false;
      base.OnPause();
    }

    private void StartFileSelect(bool makeCurrent, bool noForwardToPassword = false)
    {
      Intent intent = new Intent(this, typeof(FileSelectActivity));
      AppTask.ToIntent(intent);
      intent.PutExtra(FileSelectActivity.NoForwardToPasswordActivity, noForwardToPassword);
      intent.PutExtra("MakeCurrent", makeCurrent);
      LaunchingOther = true;
      StartActivityForResult(intent, ReqCodeOpenNewDb);
    }

    private AppTask _appTask;
    private AppTask AppTask
    {
      get { return _appTask; }
      set
      {
        _appTask = value;
        Kp2aLog.LogTask(value, MyDebugName);
      }
    }
    private OpenDatabaseAdapter _adapter;
    private MyBroadcastReceiver _intentReceiver;
    private bool _isForeground;
    private readonly List<IOConnectionInfo> _pendingBackgroundSyncs = new List<IOConnectionInfo>();

    public override void OnBackPressed()
    {
      base.OnBackPressed();
      if (PreferenceManager.GetDefaultSharedPreferences(this)
          .GetBoolean(GetString(Resource.String.LockWhenNavigateBack_key), false))
      {
        App.Kp2a.Lock();
      }
      //by leaving the app with the back button, the user probably wants to cancel the task
      //The activity might be resumed (through Android's recent tasks list), then use a NullTask:
      AppTask = new NullTask();
      if (!IsFinishing)
        Finish();
    }

    public override void Finish()
    {
      Kp2aLog.Log($"Finishing {MyDebugName}");
      base.Finish();
    }

    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
      base.OnActivityResult(requestCode, resultCode, data);

      Kp2aLog.Log($"{MyDebugName}: OnActivityResult " + resultCode + "/" + requestCode);

      AppTask appTask = null;
      if (AppTask.TryGetFromActivityResult(data, ref appTask))
        this.AppTask = appTask;

      if (requestCode == ReqCodeOpenNewDb)
      {
        switch ((int)resultCode)
        {
          case (int)Result.Ok:

            string iocString = data?.GetStringExtra("ioc");
            IOConnectionInfo ioc = IOConnectionInfo.UnserializeFromString(iocString);

            //we first store the required sync operation and delay its execution until we loaded all AutoOpen entries (from local file)
            //if required
            bool requiresSubsequentSync = data?.GetBooleanExtra("requiresSubsequentSync", false) ?? false;
            if (requiresSubsequentSync)
            {
              _pendingBackgroundSyncs.Add(ioc);
            }

            if (App.Kp2a.TrySelectCurrentDb(ioc))
            {
              if (OpenAutoExecEntries(App.Kp2a.CurrentDb)) return;
              LaunchingOther = true;
              AppTask.CanActivateSearchViewOnStart = true;

              foreach (var pendingSyncIoc in _pendingBackgroundSyncs)
              {
                try
                {
                  var dbForIoc = App.Kp2a.TryGetDatabase(pendingSyncIoc);
                  if (dbForIoc == null)
                  {
                    continue;
                  }
                  new SyncUtil(this).StartSynchronizeDatabase(dbForIoc, false);
                }
                catch (Exception e)
                {
                  App.Kp2a.ShowMessage(this, "Failed to synchronize database", MessageSeverity.Error);
                  Kp2aLog.LogUnexpectedError(e);
                }
              }

              _pendingBackgroundSyncs.Clear();

              AppTask.LaunchFirstGroupActivity(this);
            }


            break;
          case PasswordActivity.ResultSelectOtherFile:
            StartFileSelect(true, true);
            break;
          case (int)Result.Canceled:
            if (App.Kp2a.OpenDatabases.Any() == false)
            {
              //don't open fileselect/password activity again
              OnBackPressed();
            }
            break;
          default:
            break;
        }

        return;
      }

      switch (resultCode)
      {
        case KeePass.ExitNormal: // Returned to this screen using the Back key
          if (App.Kp2a.OpenDatabases.Count() == 1)
          {
            OnBackPressed();
          }
          break;
        case KeePass.ExitLock:
          // The database has already been locked. No need to immediately return to quick unlock. Especially as this causes trouble for users with face unlock
          // (db immediately unlocked again) and confused some users as the biometric prompt seemed to disable the device back button or at least they didn't understand
          // why they should unlock...
          SetResult(KeePass.ExitClose);
          if (!IsFinishing)
            Finish();
          break;
        case KeePass.ExitLockByTimeout:
          //don't finish, bring up QuickUnlock
          break;
        case KeePass.ExitCloseAfterTaskComplete:
          // Do not lock the database
          SetResult(KeePass.ExitCloseAfterTaskComplete);
          if (!IsFinishing)
            Finish();
          break;
        case KeePass.ExitClose:
          SetResult(KeePass.ExitClose);
          if (!IsFinishing)
            Finish();
          break;
        case KeePass.ExitReloadDb:

          if (App.Kp2a.CurrentDb != null)
          {
            //remember the composite key for reloading:
            var compositeKey = App.Kp2a.CurrentDb.KpDatabase.MasterKey;
            var ioc = App.Kp2a.CurrentDb.Ioc;

            //lock the database:
            App.Kp2a.CloseDatabase(App.Kp2a.CurrentDb);

            LaunchPasswordActivityForReload(ioc, compositeKey);
          }

          break;
        case KeePass.ExitLoadAnotherDb:
          StartFileSelect(true, true);
          break;
      }

    }

    private void LaunchPasswordActivityForReload(IOConnectionInfo ioc, CompositeKey compositeKey)
    {
      LaunchingOther = true;
      PasswordActivity.Launch(this, ioc, compositeKey, new ActivityLaunchModeRequestCode(ReqCodeOpenNewDb), false);
    }

    public bool LaunchingOther { get; set; }


    private class MyBroadcastReceiver : BroadcastReceiver
    {
      readonly SelectCurrentDbActivity _activity;
      public MyBroadcastReceiver(SelectCurrentDbActivity activity)
      {
        _activity = activity;
      }

      public override void OnReceive(Context context, Intent intent)
      {
        switch (intent.Action)
        {
          case Intents.DatabaseLocked:
            _activity.OnLockDatabase();
            break;
          case Intent.ActionScreenOff:
            App.Kp2a.OnScreenOff();
            break;
        }
      }
    }

    private void OnLockDatabase()
    {
      //app tasks are assumed to be finished/cancelled when the database is locked
      AppTask = new NullTask();
      //in case we are the foreground activity, we won't get OnResume (in contrast to having other activities on top of us on the stack).
      //Call it to ensure we switch to QuickUnlock/fileselect
      if (_isForeground)
        OnResume();
    }
  }
}